1 package edu.jiangxin.apktoolbox.file.duplicate; 2 3 import edu.jiangxin.apktoolbox.utils.DateUtils; 4 import edu.jiangxin.apktoolbox.swing.extend.FileListPanel; 5 import edu.jiangxin.apktoolbox.swing.extend.EasyPanel; 6 import edu.jiangxin.apktoolbox.utils.Constants; 7 import edu.jiangxin.apktoolbox.utils.FileUtils; 8 import org.apache.commons.codec.digest.DigestUtils; 9 import org.apache.commons.io.FilenameUtils; 10 import org.apache.commons.lang3.StringUtils; 11 12 import javax.swing.*; 13 import javax.swing.table.DefaultTableModel; 14 import java.awt.*; 15 import java.awt.event.ActionEvent; 16 import java.awt.event.ActionListener; 17 import java.awt.event.MouseAdapter; 18 import java.awt.event.MouseEvent; 19 import java.io.*; 20 import java.util.List; 21 import java.util.*; 22 23 public class DuplicateSearchPanel extends EasyPanel { 24 25 private static final long serialVersionUID = 1L; 26 27 private JTabbedPane tabbedPane; 28 29 private JPanel optionPanel; 30 31 private FileListPanel fileListPanel; 32 33 private JCheckBox isSizeChecked; 34 private JCheckBox isFileNameChecked; 35 private JCheckBox isMD5Checked; 36 private JCheckBox isModifiedTimeChecked; 37 38 private JCheckBox isHiddenFileSearched; 39 private JCheckBox isRecursiveSearched; 40 private JTextField suffixTextField; 41 42 private JPanel resultPanel; 43 44 private JTable resultTable; 45 46 private DefaultTableModel resultTableModel; 47 48 private JButton searchButton; 49 private JButton cancelButton; 50 51 private JMenuItem openDirMenuItem; 52 private JMenuItem deleteFileMenuItem; 53 private JMenuItem deleteFilesInSameDirMenuItem; 54 private JMenuItem deleteFilesInSameDirRecursiveMenuItem; 55 56 private Thread searchThread; 57 58 final private Map<String, List<File>> duplicateFileGroupMap = new HashMap<>(); 59 60 @Override 61 public void initUI() { 62 tabbedPane = new JTabbedPane(); 63 add(tabbedPane); 64 65 createOptionPanel(); 66 tabbedPane.addTab("Option", null, optionPanel, "Show Search Options"); 67 68 createResultPanel(); 69 tabbedPane.addTab("Result", null, resultPanel, "Show Search Result"); 70 } 71 72 private void createOptionPanel() { 73 optionPanel = new JPanel(); 74 optionPanel.setLayout(new BoxLayout(optionPanel, BoxLayout.Y_AXIS)); 75 76 fileListPanel = new FileListPanel(); 77 78 JPanel checkOptionPanel = new JPanel(); 79 checkOptionPanel.setLayout(new BoxLayout(checkOptionPanel, BoxLayout.X_AXIS)); 80 checkOptionPanel.setBorder(BorderFactory.createTitledBorder("Check Options")); 81 82 isSizeChecked = new JCheckBox("Size"); 83 isSizeChecked.setSelected(true); 84 isFileNameChecked = new JCheckBox("Filename"); 85 isMD5Checked = new JCheckBox("MD5"); 86 isModifiedTimeChecked = new JCheckBox("Last Modified Time"); 87 checkOptionPanel.add(isSizeChecked); 88 checkOptionPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER)); 89 checkOptionPanel.add(isFileNameChecked); 90 checkOptionPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER)); 91 checkOptionPanel.add(isMD5Checked); 92 checkOptionPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER)); 93 checkOptionPanel.add(isModifiedTimeChecked); 94 checkOptionPanel.add(Box.createHorizontalGlue()); 95 96 JPanel searchOptionPanel = new JPanel(); 97 searchOptionPanel.setLayout(new BoxLayout(searchOptionPanel, BoxLayout.X_AXIS)); 98 searchOptionPanel.setBorder(BorderFactory.createTitledBorder("Search Options")); 99 100 isHiddenFileSearched = new JCheckBox("Hidden Files"); 101 isRecursiveSearched = new JCheckBox("Recursive"); 102 isRecursiveSearched.setSelected(true); 103 JLabel suffixLabel = new JLabel("Suffix: "); 104 suffixTextField = new JTextField(); 105 suffixTextField.setToolTipText("an array of extensions, ex. {\"java\",\"xml\"}. If this parameter is empty, all files are returned."); 106 searchOptionPanel.add(isHiddenFileSearched); 107 searchOptionPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER)); 108 searchOptionPanel.add(isRecursiveSearched); 109 searchOptionPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER)); 110 searchOptionPanel.add(suffixLabel); 111 searchOptionPanel.add(suffixTextField); 112 searchOptionPanel.add(Box.createHorizontalGlue()); 113 114 JPanel operationPanel = new JPanel(); 115 operationPanel.setLayout(new BoxLayout(operationPanel, BoxLayout.X_AXIS)); 116 operationPanel.setBorder(BorderFactory.createTitledBorder("Operations")); 117 118 searchButton = new JButton("Search"); 119 cancelButton = new JButton("Cancel"); 120 searchButton.addActionListener(new OperationButtonActionListener()); 121 cancelButton.addActionListener(new OperationButtonActionListener()); 122 operationPanel.add(searchButton); 123 operationPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER)); 124 operationPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER)); 125 operationPanel.add(cancelButton); 126 operationPanel.add(Box.createHorizontalGlue()); 127 128 optionPanel.add(fileListPanel); 129 optionPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER)); 130 optionPanel.add(checkOptionPanel); 131 optionPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER)); 132 optionPanel.add(searchOptionPanel); 133 optionPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER)); 134 optionPanel.add(operationPanel); 135 } 136 137 private void createResultPanel() { 138 resultPanel = new JPanel(); 139 resultPanel.setLayout(new BoxLayout(resultPanel, BoxLayout.Y_AXIS)); 140 141 resultTableModel = new DuplicateFilesTableModel(new Vector<>(), DuplicateFilesConstants.COLUMN_NAMES); 142 resultTable = new JTable(resultTableModel); 143 144 resultTable.setDefaultRenderer(Vector.class, new DuplicateFilesTableCellRenderer()); 145 146 for (int i = 0; i < resultTable.getColumnCount(); i++) { 147 resultTable.getColumn(resultTable.getColumnName(i)).setCellRenderer(new DuplicateFilesTableCellRenderer()); 148 } 149 150 resultTable.addMouseListener(new MyMouseListener()); 151 152 resultTable.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION); 153 154 JScrollPane scrollPane = new JScrollPane(resultTable); 155 resultPanel.add(scrollPane); 156 } 157 158 public String getComparedKey(File file) { 159 StringBuilder sb = new StringBuilder(); 160 if (isSizeChecked.isSelected()) { 161 sb.append("[Size]["); 162 sb.append(DigestUtils.md5Hex(String.valueOf(file.length()))); 163 sb.append("]"); 164 } 165 if (isFileNameChecked.isSelected()) { 166 sb.append("[Filename]["); 167 sb.append(DigestUtils.md5Hex(file.getName())); 168 sb.append("]"); 169 } 170 if (isMD5Checked.isSelected()) { 171 sb.append("[MD5]["); 172 try (InputStream is = new FileInputStream(file)) { 173 sb.append(DigestUtils.md5Hex(is)); 174 } catch (FileNotFoundException e) { 175 logger.error("getComparedKey FileNotFoundException"); 176 } catch (IOException e) { 177 logger.error("getComparedKey IOException"); 178 } 179 sb.append("]"); 180 } 181 if (isModifiedTimeChecked.isSelected()) { 182 sb.append("[ModifiedTime]["); 183 sb.append(DigestUtils.md5Hex(String.valueOf(file.lastModified()))); 184 sb.append("]"); 185 } 186 logger.info("path: " + file.getAbsolutePath() + ", key: " + sb); 187 return sb.toString(); 188 } 189 190 class MyMouseListener extends MouseAdapter { 191 @Override 192 public void mouseReleased(MouseEvent e) { 193 super.mouseReleased(e); 194 int r = resultTable.rowAtPoint(e.getPoint()); 195 if (r >= 0 && r < resultTable.getRowCount()) { 196 resultTable.setRowSelectionInterval(r, r); 197 } else { 198 resultTable.clearSelection(); 199 } 200 int rowIndex = resultTable.getSelectedRow(); 201 if (rowIndex < 0) { 202 return; 203 } 204 if (e.isPopupTrigger() && e.getComponent() instanceof JTable) { 205 JPopupMenu popupmenu = new JPopupMenu(); 206 MyMenuActionListener menuActionListener = new MyMenuActionListener(); 207 208 openDirMenuItem = new JMenuItem("Open parent folder of this file"); 209 openDirMenuItem.addActionListener(menuActionListener); 210 popupmenu.add(openDirMenuItem); 211 212 deleteFileMenuItem = new JMenuItem("Delete this duplicate file"); 213 deleteFileMenuItem.addActionListener(menuActionListener); 214 popupmenu.add(deleteFileMenuItem); 215 216 deleteFilesInSameDirMenuItem = new JMenuItem("Delete these duplicate files in the same directory"); 217 deleteFilesInSameDirMenuItem.addActionListener(menuActionListener); 218 popupmenu.add(deleteFilesInSameDirMenuItem); 219 220 deleteFilesInSameDirRecursiveMenuItem = new JMenuItem("Delete these duplicate files in the same directory(Recursive)"); 221 deleteFilesInSameDirRecursiveMenuItem.addActionListener(menuActionListener); 222 popupmenu.add(deleteFilesInSameDirRecursiveMenuItem); 223 224 popupmenu.show(e.getComponent(), e.getX(), e.getY()); 225 } 226 } 227 } 228 229 class MyMenuActionListener implements ActionListener { 230 @Override 231 public void actionPerformed(ActionEvent actionEvent) { 232 Object source = actionEvent.getSource(); 233 if (source.equals(openDirMenuItem)) { 234 onOpenDir(); 235 } else if (source.equals(deleteFileMenuItem)) { 236 onDeleteFile(); 237 } else if (source.equals(deleteFilesInSameDirMenuItem)) { 238 onDeleteFilesInSameDir(); 239 } else if (source.equals(deleteFilesInSameDirRecursiveMenuItem)) { 240 onDeleteFilesInSameDirRecursive(); 241 } else { 242 logger.error("invalid source"); 243 } 244 } 245 246 private void onOpenDir() { 247 int rowIndex = resultTable.getSelectedRow(); 248 String parentPath = resultTableModel.getValueAt(rowIndex, resultTable.getColumn(DuplicateFilesConstants.COLUMN_NAME_FILE_PARENT).getModelIndex()).toString(); 249 File parent = new File(parentPath); 250 if (parent.isDirectory()) { 251 try { 252 Desktop.getDesktop().open(parent); 253 } catch (IOException e) { 254 logger.error("open parent failed: " + parent.getPath()); 255 } 256 } 257 } 258 259 private void onDeleteFile() { 260 int rowIndex = resultTable.getSelectedRow(); 261 String parentPath = resultTableModel.getValueAt(rowIndex, resultTable.getColumn(DuplicateFilesConstants.COLUMN_NAME_FILE_PARENT).getModelIndex()).toString(); 262 String name = resultTableModel.getValueAt(rowIndex, resultTable.getColumn(DuplicateFilesConstants.COLUMN_NAME_FILE_NAME).getModelIndex()).toString(); 263 File selectedFile = new File(parentPath, name); 264 String key = getComparedKey(selectedFile); 265 List<File> files = duplicateFileGroupMap.get(key); 266 for (File file : files) { 267 if (!selectedFile.equals(file)) { 268 continue; 269 } 270 files.remove(file); 271 boolean isSuccessful = file.delete(); 272 logger.info("delete file: " + file.getAbsolutePath() + ", result: " + isSuccessful); 273 break; 274 } 275 resultTableModel.setRowCount(0); 276 showResult(); 277 } 278 279 private void onDeleteFilesInSameDir() { 280 int rowIndex = resultTable.getSelectedRow(); 281 String parentPath = resultTableModel.getValueAt(rowIndex, resultTable.getColumn(DuplicateFilesConstants.COLUMN_NAME_FILE_PARENT).getModelIndex()).toString(); 282 for (Map.Entry<String, List<File>> entry : duplicateFileGroupMap.entrySet()) { 283 List<File> duplicateFileGroup = entry.getValue(); 284 for (File duplicateFile : duplicateFileGroup) { 285 String parentPathTmp = duplicateFile.getParent(); 286 if (Objects.equals(parentPath, parentPathTmp)) { 287 duplicateFileGroup.remove(duplicateFile); 288 boolean isSuccessful = duplicateFile.delete(); 289 logger.info("delete file: " + duplicateFile.getAbsolutePath() + ", result: " + isSuccessful); 290 break; 291 } 292 } 293 } 294 resultTableModel.setRowCount(0); 295 showResult(); 296 } 297 298 private void onDeleteFilesInSameDirRecursive() { 299 int rowIndex = resultTable.getSelectedRow(); 300 String parentPath = resultTableModel.getValueAt(rowIndex, resultTable.getColumn(DuplicateFilesConstants.COLUMN_NAME_FILE_PARENT).getModelIndex()).toString(); 301 for (Map.Entry<String, List<File>> entry : duplicateFileGroupMap.entrySet()) { 302 List<File> duplicateFileGroup = entry.getValue(); 303 for (File duplicateFile : duplicateFileGroup) { 304 String parentPathTmp = duplicateFile.getParent(); 305 if (Objects.equals(parentPath, parentPathTmp) || FilenameUtils.directoryContains(parentPath, parentPathTmp)) { 306 duplicateFileGroup.remove(duplicateFile); 307 boolean isSuccessful = duplicateFile.delete(); 308 logger.info("delete file: " + duplicateFile.getAbsolutePath() + ", result: " + isSuccessful); 309 break; 310 } 311 } 312 } 313 resultTableModel.setRowCount(0); 314 showResult(); 315 } 316 } 317 318 class OperationButtonActionListener implements ActionListener { 319 @Override 320 public void actionPerformed(ActionEvent e) { 321 Object source = e.getSource(); 322 if (source.equals(searchButton)) { 323 String[] extensions = null; 324 if (StringUtils.isNotEmpty(suffixTextField.getText())) { 325 extensions = suffixTextField.getText().split(","); 326 } 327 searchThread = new SearchThread(extensions, isRecursiveSearched.isSelected(), isHiddenFileSearched.isSelected(), duplicateFileGroupMap); 328 searchThread.start(); 329 } else if (source.equals(cancelButton)) { 330 if (searchThread.isAlive()) { 331 searchThread.interrupt(); 332 } 333 } 334 335 } 336 } 337 338 private void showResult() { 339 SwingUtilities.invokeLater(() -> { 340 int groupIndex = 0; 341 for (Map.Entry<String, List<File>> entry : duplicateFileGroupMap.entrySet()) { 342 List<File> duplicateFileGroup = entry.getValue(); 343 if (duplicateFileGroup.size() < 2) { 344 continue; 345 } 346 groupIndex++; 347 for (File duplicateFile : duplicateFileGroup) { 348 Vector<Object> rowData = getRowVector(groupIndex, duplicateFile); 349 resultTableModel.addRow(rowData); 350 } 351 } 352 tabbedPane.setSelectedIndex(1); 353 }); 354 } 355 356 private Vector<Object> getRowVector(int groupIndex, File file) { 357 Vector<Object> rowData = new Vector<>(); 358 rowData.add(groupIndex); 359 rowData.add(file.getParent()); 360 rowData.add(file.getName()); 361 rowData.add(FilenameUtils.getExtension(file.getName())); 362 rowData.add(FileUtils.sizeOfInHumanFormat(file)); 363 rowData.add(DateUtils.millisecondToHumanFormat(file.lastModified())); 364 return rowData; 365 } 366 367 class SearchThread extends Thread { 368 private String[] extensions; 369 private boolean isRecursiveSearched; 370 private boolean isHiddenFileSearched; 371 private Map<String, List<File>> duplicateFileGroupMap; 372 373 public SearchThread(String[] extensions, boolean isRecursiveSearched, boolean isHiddenFileSearched, Map<String, List<File>> duplicateFileGroupMap) { 374 super(); 375 this.extensions = extensions; 376 this.isRecursiveSearched = isRecursiveSearched; 377 this.isHiddenFileSearched = isHiddenFileSearched; 378 this.duplicateFileGroupMap = duplicateFileGroupMap; 379 } 380 381 @Override 382 public void run() { 383 super.run(); 384 duplicateFileGroupMap.clear(); 385 List<File> fileList = fileListPanel.getFileList(); 386 387 Set<File> fileSet = new TreeSet<>(fileList); 388 for (File file : fileList) { 389 fileSet.addAll(org.apache.commons.io.FileUtils.listFiles(file, extensions, isRecursiveSearched)); 390 } 391 392 for (File file : fileSet) { 393 if (Thread.currentThread().isInterrupted()) { 394 break; 395 } 396 if (file.isHidden() && !isHiddenFileSearched) { 397 continue; 398 } 399 String hash = getComparedKey(file); 400 List<File> list = duplicateFileGroupMap.computeIfAbsent(hash, k -> new LinkedList<>()); 401 list.add(file); 402 } 403 showResult(); 404 } 405 } 406 }